/**
 * This software was produced for the U. S. Government
 * under Contract No. FA8721-09-C-0002, and is
 * subject to the Rights in Noncommercial Computer Software
 * and Noncommercial Computer Software Documentation Clause
 * (DFARS) 252.227-7014 (JUN 1995)
 * 
 * (c) 2009 The MITRE Corporation. All Rights Reserved.
 *
 * SPMIF to OPNET Translator
 * Author: EWALTERS
 * Created: 12:55:42 PM, Aug 2, 2007
 * MITRE Corporation
 */
package com.mitre.spmif2opnet.analysis.ast;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import com.mitre.spmif2opnet.generation.GenerationException;
import com.mitre.spmif2opnet.generation.ir.PhysicalNode;

/**
 * S-PMIF Overhead matrix - optimized for our purposes
 */
public class Overhead {

	/**
	 * Represents an (device, service amount) pair
	 */
	public static class OverheadEntry {
		private double _amount;
		private String _device;
		
		public OverheadEntry(double amount, String device) {
			_amount = amount;
			_device = device;
		}
		
		public double getAmount() {
			return _amount;
		}
		
		public String getDeviceId() {
			return _device;
		}
	}
	
	/**
	 * List of overhead entries, keyed by resource name
	 */
	private Hashtable<String, List<OverheadEntry>> _om_matrix;
	/**
	 * List of devices
	 */
	private Hashtable<String, Device> _devices;
	/**
	 * List of resources
	 */
	private Hashtable<String, String> _resources;
	
	/**
	 * Default constructor
	 */
	public Overhead() {
		_om_matrix = new Hashtable<String, List<OverheadEntry>>();
		_devices   = new Hashtable<String, Device>();
		_resources = new Hashtable<String, String>();
	}
	
	/**
	 * Add a device to the overhead
	 * @param dev the device
	 */
	public void addDevice(Device dev) {
		if (_devices.containsKey(dev.getId())) {
			System.out.println("[WARNING] Duplicate device " + dev.getId() + " encountered in overhead!");
		}
		_devices.put(dev.getId(), dev);
	}
	
	/**
	 * Add a raw resource to the overhead
	 *  NOTE: We assume the devices are already present 
	 * @param resource_id  the resource id
	 * @param resource_name  the resource name
	 * @param amount how much of the resource
	 * @param device_id the device used
	 */
	public void addOverhead(String resource_id, String resource_name, double amount, String device_id) {
		if (!_devices.containsKey(device_id)) {
			System.out.println("[ERROR] Invalid device name " + device_id 
					+ " in overhead for resource " + resource_id);
			System.exit(-1);
		}
		OverheadEntry entry = new OverheadEntry(amount, device_id);
		List<OverheadEntry> l;
		if (_om_matrix.containsKey(resource_id)) {
			l = _om_matrix.get(resource_id); 
		} else {
			l = new ArrayList<OverheadEntry>();
		}
		l.add(entry);
		_om_matrix.put(resource_id, l);
		_resources.put(resource_id, resource_name);
	}

	/**
	 * Get the overhead for a particular resources
	 * @param resource_id which resource
	 * @return the overhead
	 */
	public List<OverheadEntry> getResources(String resource_id) {
		return _om_matrix.get(resource_id);
	}

	/**
	 * Get the resource name from an id
	 * @param resource_id the resource id
	 * @return the name
	 */
	public String getResourceName(String resource_id) {
		return _resources.get(resource_id);
	}
	
	/**
	 * Get all of the devices used by a given resource id
	 * @param resource_id the id
	 * @return the devices
	 */
	public List<Device> getDevices(String resource_id) {
		List<Device> devices = new LinkedList<Device>();
		for (OverheadEntry overent : this.getResources(resource_id)) {
			devices.add(_devices.get(overent.getDeviceId()));
		}
		return devices;
	}
	
	/**
	 * Get the device from the device id
	 * @param device_id the id
	 * @return the device
	 */
	public Device getDevice(String device_id) {
		if (!_devices.containsKey(device_id)) {
			System.out.println("[ERROR] Invalid device name " + device_id);
			System.exit(-1);
		}
		return _devices.get(device_id);
	}

	/** 
	 * Determine if a resource uses a device from a given category
	 * @param res_id id
	 * @param cat which category to use
	 * @return yes or no
	 */
	public boolean isResourceType(String res_id, Device.DeviceCategory cat) {
		for (Device d : this.getDevices(res_id)) {
			if (d.isCategory(cat)) return true;
		}
		return false;
	}
	
	/**
	 * Does this resource exist in the overhead matrix?
	 * @param resource_id the resource
	 * @return yes or no
	 */
	public boolean resourceExists(String resource_id) {
		return _om_matrix.containsKey(resource_id);
	}
	
	/**
	 * Get the resource id for a given node and a given device category
	 * NOTE: Here we make the explicit assumption that there is only >ONE< 
	 *  processing resource per node
	 *  If there is more than one, it is unclear how we can distribute the phase
	 *   to OPNET nodes!
	 * BIG NOTE: We have run into instances where this assumption is not true - how 
	 *  do we handle it?
	 * @param n the node
	 * @param cat the device category
	 * @return the resource id
	 * @throws GenerationException
	 */
	public String getResourceId(Node n, Device.DeviceCategory cat) throws GenerationException {		
		// We take the name of the processing resource
		// If there are multiple, we will get funny messages when we try to deploy
		String name = null;
		for (String res : n.getResources()) {
			if (this.isResourceType(res, cat)) {
				if (name != null) {
					throw new GenerationException("[ERROR] Multiple " + cat.toString() + " Resources for node " 
							+ n.getName());
				}
				name = res;
			}
		}
		return name;
	}
	
	/**
	 * Get the timing for a given node and category
	 * @param n the node
	 * @param cat the category
	 * @return the timing
	 * @throws GenerationException
	 */
	public double getResourceTiming(Node n, Device.DeviceCategory cat) throws GenerationException {
		String res_id = getResourceId(n, cat);
		if (res_id == null) return 0.0;
		double res_service = n.getResourceService(res_id);

		double om_factor = 0.0; 
		double dev_service = 0.0; 		
		boolean device_found = false;
		for (Overhead.OverheadEntry entry : this.getResources(res_id)) {
			Device d = this.getDevice(entry.getDeviceId());
			ServiceUnitType units = d.getServiceUnits();
			if (d.isCategory(cat) && device_found) {
				throw new GenerationException("[ERROR] Multiple Processing Devices for node "
						+ n.getName() + ", resource " + res_id);
			} else {
				res_service = units.normalize(res_service);
				device_found = true;
				om_factor = entry.getAmount();
				dev_service = d.getServiceTime();
			}
		}
		
		return res_service * om_factor * dev_service;
	}

	/**
	 * This method is for grabbing the device name to store in the job specification
	 * @param res_id resource id
	 * @param cat device category
	 * @return device name
	 * @throws GenerationException
	 */
	public String getDeviceId(String res_id, Device.DeviceCategory cat) throws GenerationException {
		String dev_id = "NONE";
		boolean device_found = false;
		
		for (OverheadEntry entry : this.getResources(res_id)) {
			Device d = _devices.get(entry.getDeviceId());
			if (d.isCategory(cat) && device_found) {
				throw new GenerationException("[ERROR] Multiple Devices of same category found for " 
						+ "resource " + res_id);
			} else {
				dev_id = d.getId();
				device_found = true;
			}
		}
		return dev_id;
	}

	/**
	 * Post-processing to create a list of physical devices for the IR
	 * @return the physical devices
	 */
	public Hashtable<String, PhysicalNode> createDevices() {
		Hashtable<String, PhysicalNode> pnodes = new Hashtable<String, PhysicalNode>();
		// Load up the devices - only processing ones!
		for (Device d : _devices.values()) {
			if (d.isProcessingDevice()) {
				PhysicalNode pnode = new PhysicalNode(d);
				pnodes.put(d.getId(), pnode);
			}
		}
		
		// Allocate the resources - we will use this later
		for (String res_id : _om_matrix.keySet()) {
			for (OverheadEntry entry : _om_matrix.get(res_id)) {
				String dev_id = entry.getDeviceId();
				for (PhysicalNode pnode : pnodes.values()) {
					if (pnode.getId().equals(dev_id)) {
						pnode.addMyResource(res_id);
					} else if (_devices.get(dev_id).isProcessingDevice()){
						pnode.addOtherResource(res_id, dev_id);
					}
				}
			}
		}
		return pnodes;
	}
}
